#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import datetime
import glob
import json
import locale
import msvcrt
import os
import re
import shlex
import shutil
import stat
import subprocess
import time
import uuid

import psutil

from common.common import check_command_injection_exclude_quote, execute_cmd, get_host_sn, invoke_rpc_tool_interface, \
    write_content_to_file
from common.common_models import ActionResult, LogDetail
from common.const import SysData, RpcParamKey, CMDResult, DBLogLevel
from common.env_common import adaptation_win_path, get_install_head_path
from common.util.exec_utils import exec_overwrite_file, exec_mkdir_cmd
from exchange import log
from exchange.commons.command import Commands
from exchange.commons.const import ParamConstant, ExchangeCode, ExchangeWindowsPath, LastCopyType, \
    ExchangeServiceStatus, CreateSoftLink
from exchange.commons.exchange_exception import ExchangeErrCodeException, ExchangeInternalException
from exchange.commons.powershell import execute, execute_without_auth, execute_with_output_bool



def query_job_permission(self):
    output_action_result(self.pid, ExchangeCode.SUCCESS.value, 0, "")
    return True


def check_sh_cmd_param(param):
    """
    检测拼接shell命令的参数是否含有特殊字符，防止命令注入
    :param  : 拼接 shell 的参数
    :return : bool
    """
    if not param or not isinstance(param, str):
        return False
    return not check_command_injection_exclude_quote(param)


def check_path_legal(file_path):
    real_dir_path = os.path.realpath(file_path)
    if real_dir_path.startswith(ParamConstant.MNT_PATH):
        return
    system_drive_up = os.getenv("SystemDrive").upper()
    system_drive_down = system_drive_up.lower()
    if real_dir_path.startswith(system_drive_up) or \
            real_dir_path.startswith(system_drive_down):
        if os.path.dirname(real_dir_path) != os.path.realpath(ParamConstant.RESULT_PATH) \
                and os.path.dirname(real_dir_path) != os.path.realpath(ParamConstant.PARAM_FILE_PATH):
            log.error("File path not support to write")
            raise Exception("File path not support to write")


def output_execution_result_ex(file_path, payload):
    # 先将文件清空再写
    check_path_legal(file_path)
    json_str = json.dumps(payload, ensure_ascii=False)
    flags = os.O_WRONLY | os.O_CREAT
    modes = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
    with os.fdopen(os.open(file_path, flags, modes), 'w') as out_file:
        out_file.truncate(0)
        out_file.write(json_str)


def output_result_file(pid, result_info):
    if not pid or not result_info:
        raise Exception("Param pid or result info is none")
    output_file_name = "{}{}".format("result", pid)
    output_file_path = os.path.join(ParamConstant.RESULT_PATH, output_file_name)
    output_execution_result(output_file_path, result_info)
    if not os.path.exists(output_file_path):
        raise Exception(f"Result file: {output_file_path} can not create")


def output_execution_result(file_path, payload):
    json_str = json.dumps(payload)
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
    modes = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
    with os.fdopen(os.open(file_path, flags, modes), 'w') as f:
        f.write(json_str)


def get_snap_id(snap_infos, device):
    for snap_info_data in snap_infos['snapInfo']:
        device_letter = snap_info_data['deviceLetter']
        volume_id = snap_info_data['volumeId']
        if device_letter[0] == device:
            return volume_id
    return ""


def get_command_result(command):
    tmp_code = locale.getdefaultlocale()[1]
    if "|" in command:
        raise Exception("Special characters exist.")

    process = subprocess.Popen(shlex.split(command), stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE, encoding=tmp_code, errors="ignore")
    process.wait()
    code, out, err = str(process.returncode), process.stdout.read(), process.stderr.read()
    return code, out, err


def get_vss_check_command_result(command):
    tmp_code = locale.getdefaultlocale()[1]
    if "|" in command:
        raise Exception("Special characters exist.")

    process = subprocess.Popen(shlex.split(command), stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE, encoding=tmp_code, errors="ignore")
    process.communicate()
    code, out, err = str(process.returncode), process.stdout, process.stderr
    return code, out, err


def find_db_list_files(root_dir):
    # 获取root_dir目录及其所有子目录下的db_list.json文件路径
    pattern = os.path.join(root_dir, '**', 'db_list.json')
    db_list_files = glob.glob(pattern, recursive=True)
    return db_list_files


def read_json_file(file_path):
    # 读取并返回JSON文件内容
    with open(file_path, 'r', encoding='utf-8') as file:
        return json.load(file)


def get_last_copy_info(job_info, all_param, copy_type):
    # 获取上次数据备份副本信息
    log.info(f"start get_last_copy_info copy type:{copy_type}")
    last_copy_type = LastCopyType.last_copy_type_dict.get(copy_type)
    input_param = {
        RpcParamKey.APPLICATION: all_param.get("job", {}).get("protectObject"),
        RpcParamKey.TYPES: last_copy_type,
        RpcParamKey.COPY_ID: "",
        RpcParamKey.JOB_ID: job_info.job_id
    }
    try:
        result = invoke_rpc_tool_interface(job_info.job_id, RpcParamKey.QUERY_PREVIOUS_CPOY, input_param)
    except Exception as err_info:
        log.error(f"Get last copy info fail.{err_info}")
        return {}
    return result


def init_hub_path(data_hub, meta_hub):
    # 拼接/rep/{agent_id}路径并初始化
    agent_id = get_host_sn()
    data_hub = data_hub.replace("/", "\\") + f"\\{agent_id}"
    meta_hub = meta_hub.replace("/", "\\") + f"\\{agent_id}"
    init_cache_and_data_directory(data_hub, meta_hub)
    return data_hub, meta_hub


def init_cache_and_data_directory(data_hub_path, meta_hub_path):
    path_list = [data_hub_path, meta_hub_path]
    for path in path_list:
        final_path = ExchangeWindowsPath.UNC + path
        final_path = final_path.replace("/", "\\")
        try:
            if not os.path.exists(final_path):
                log.info(f"begin create path !path:{final_path}")
                os.makedirs(final_path)
        except Exception as error:
            log.error(error, exc_info=True)
            log.info("init_cache_and_data_directory FAILED!")
            raise ExchangeErrCodeException(log_detail=LogDetail(
                logDetail="",
                logDetailParam=[""],
                logLevel=DBLogLevel.ERROR)) from error
    return True


def init_directory(path):
    final_path = ExchangeWindowsPath.UNC + path
    final_path = final_path.replace("/", "\\")
    try:
        if not os.path.exists(final_path):
            log.info(f"begin create path !path:{final_path}")
            os.makedirs(final_path)
    except Exception as error:
        log.error(error, exc_info=True)
        log.info("init_cache_and_data_directory FAILED!")
        raise ExchangeErrCodeException(log_detail=LogDetail(
            logDetail="",
            logDetailParam=[""],
            logLevel=DBLogLevel.ERROR)) from error


def get_databases(m_username, m_password):
    """
    功能描述：获取数据库信息
    查询是否有可以备份的数据库：
    数据状态为已挂载，且状态为healthy
    """
    # "\" 转为 "\\"
    username = m_username.replace("\\", "\\\\")
    # 密码中特殊字符$转义
    m_password = m_password.replace("$", "\`\$")
    # 获取单机或DGA上所有已挂载的数据库
    ps1_path = ParamConstant.PS1_SCRIPTS_PATH + "GetAllMountedMailboxDatabases.ps1"
    cmds = f"{ParamConstant.PS_LOC} {ps1_path} \\\"{username}\\\" \\\"{m_password}\\\""
    ret, std_out, std_err = execute_cmd(cmds)
    result = '\n'.join(std_out.splitlines()[-1:])
    result = json.loads(result)
    return_code = result.get("ReturnCode", "")
    resource_list = []
    # 盘符列表
    drive_letter = []
    # 数据库GUID列表
    guids = []
    agent_id = get_host_sn()
    if return_code != "0":
        return resource_list, drive_letter, guids
    if result.get("Output", []):
        for temp in result.get("Output"):
            resource = {
                "name": temp["db_name"], "activeCopy": True, "uuid": temp["db_uuid"], "agentId": agent_id,
                "logFilePrefix": temp["log_file_prefix"], "extendInfo": temp
            }
            drive_letter.append(temp["edb_path"][0])
            drive_letter.append(temp["log_path"][0])
            guids.append(temp["db_uuid"])
            resource_list.append(resource)
    drive_letter = list(set(drive_letter))
    return resource_list, drive_letter, guids


def get_passive_databases(m_username, m_password, host_name):
    """
    功能描述：获取数据库被动副本信息
    查询是否有可以备份的数据库：
    数据状态为已挂载，且状态为healthy
    """
    log.info(f"get passive databases start!")
    log.info(f"local host name is :{host_name}")
    resource_list = []
    # 盘符列表
    drive_letter = []
    guids = []
    # 获取本节点exchange服务上的所有数据库信息
    db_names = get_mailbox_database_by_server(m_username, m_password, host_name)
    # 获取本节点exchange服务上的所有公共数据库信息
    public_folder_db_names = get_public_folder_database_by_server(m_username, m_password, host_name)
    # 合并数据库名称
    db_names = db_names + public_folder_db_names
    if not db_names:
        log.info(f"local agent backup database is empty:{len(db_names)}")
        return resource_list, drive_letter, guids
    # 获取被动数据库副本信息
    passive_copy_db_names = get_mailbox_database_passive_copy(m_username, m_password, host_name, db_names)
    # 获取节点被动副本的盘符信息
    resource_list, drive_letter, guids = get_database_path(m_username, m_password, passive_copy_db_names)
    return resource_list, drive_letter, guids


def get_database_path(m_username, m_password, db_names):
    """
    功能描述：获取数据库的edb_path,log_path和guid
    :return 数据库副本路径信息，去重的路径盘符
    """
    log.info(f"get database path start!")
    drive_letter = []
    guids = []
    database_list = []
    for db_name in db_names:
        script = Commands.GET_MAILBOX_DATABASE_BASE_INFO.format(db_name=db_name["name"])
        result = execute(m_username, m_password, script)
        # 盘符列表
        return_code = result.get("ReturnCode", "")
        if return_code != "0":
            log.error("Get Mailbox Database ReturnCode: %s, ErrorMessage: %s", return_code, result.get("Output", ""))
            return database_list, drive_letter, guids
        if result.get("Output", []):
            result_obj = json.loads(result.get("Output"))
            # 返回结果只有一条时，创建一个list
            if not isinstance(result_obj, list):
                result_obj = [result_obj]
            for database in result_obj:
                temp = dict()
                temp["edb_path"] = database["EdbFilePath"]["PathName"]
                temp["log_path"] = database["LogFolderPath"]["PathName"]
                database_info = {
                    "name": db_name["name"], "activeCopy": db_name["activeCopy"], "uuid": database["Guid"],
                    "agentId": get_host_sn(), "logFilePrefix": database["LogFilePrefix"], "extendInfo": temp
                }
                drive_letter.append(database["EdbFilePath"]["PathName"][0])
                drive_letter.append(database["LogFolderPath"]["PathName"][0])
                guids.append(database["Guid"])
                database_list.append(database_info)
    drive_letter = list(set(drive_letter))
    log.info(f"get database path success!")
    return database_list, drive_letter, guids


def get_mailbox_database_passive_copy(m_username, m_password, host_name, db_names):
    """
    功能描述：获取数据库信息副本信息
    查询指定数据库下：
    1.数据副本状态为已挂载或状态为healthy
    2.获取最大激活选项被动副本，判断是否属于本节点
    3.不存在被动副本，则选择该节点上的主副本
    """
    log.info(f"get mailbox database passive copy start!")
    backup_database_name = []
    for db_name in db_names:
        script = Commands.GET_MAILBOX_DATABASE_COPY_STATUS.format(db_name=db_name["name"])
        result = execute(m_username, m_password, script)
        copy_list = []
        return_code = result.get("ReturnCode", "")
        if return_code != "0":
            log.error("Get Mailbox ReturnCode: %s, ErrorMessage: %s", return_code, result.get("Output", ""))
            return copy_list
        if result.get("Output", []):
            get_max_preference_copy_table(backup_database_name, copy_list, db_name, host_name, result)
    log.info(f"get mailbox database passive copy success!")
    return backup_database_name


def get_max_preference_copy_table(backup_database_name, copy_list, db_name, host_name, result):
    result_obj = json.loads(result.get("Output"))
    # 返回结果只有一条时，创建一个list
    if not isinstance(result_obj, list):
        result_obj = [result_obj]
    active_copy_info = dict()
    for database_copy in result_obj:
        copy_info = dict()
        status = database_copy["Status"]["Value"]
        # 仅获取状态为 Healthy和Mounted的副本信息,获取激活首选项最大的副本
        if status not in ("Healthy", "Mounted"):
            continue
        # 当前副本为主动副本跳过
        active_copy = database_copy["ActiveCopy"]
        if active_copy:
            active_copy_info["mailboxServer"] = database_copy["MailboxServer"]
            active_copy_info["activeDatabaseCopy"] = database_copy["ActiveDatabaseCopy"]
            continue
        copy_info["mailboxServer"] = database_copy["MailboxServer"].upper()
        copy_info["activationPreference"] = database_copy["ActivationPreference"]
        copy_list.append(copy_info)
    if copy_list:
        # 激活首选项相同取查询序列第一个副本
        highest_activation_preference_copy = min(copy_list, key=lambda x: x['activationPreference'])
        # 如果激活首选项最大的副本在本节点，则在本节点备份
        if highest_activation_preference_copy["mailboxServer"].upper() == host_name.upper():
            backup_info = dict()
            backup_info["name"] = db_name["name"]
            backup_info["activeCopy"] = False
            backup_database_name.append(backup_info)
    elif active_copy_info:
        # 无可备份的被动副本，判断主副本是否在本节点
        if active_copy_info["mailboxServer"].upper() == host_name.upper():
            backup_info = dict()
            backup_info["name"] = db_name["name"]
            backup_info["activeCopy"] = True
            backup_database_name.append(backup_info)


def get_mailbox_database_by_server(m_username, m_password, host_name):
    """
    功能描述：获取指定节点的数据库信息
    查询是否有可以备份的数据库：
    Recovery 不备份
    """
    log.info(f"get mailbox database by server start!")
    script = Commands.GET_MAILBOX_DATABASE_BY_SERVER.format(server=host_name)
    result = execute(m_username, m_password, script)
    database_list = []
    return_code = result.get("ReturnCode", "")
    if return_code != "0":
        log.error("Get Mailbox Database ReturnCode: %s, ErrorMessage: %s", return_code, result.get("Output", ""))
        return database_list
    if result.get("Output", []):
        result_obj = json.loads(result.get("Output"))
        # 返回结果只有一条时，创建一个list
        if not isinstance(result_obj, list):
            result_obj = [result_obj]
        for database in result_obj:
            database_info = dict()
            # 不备份Recovery 数据库
            recovery = database["Recovery"]
            if recovery:
                continue
            database_info["server"] = database["Server"]
            database_info["name"] = database["Name"]
            database_list.append(database_info)
    log.info(f"get mailbox database by server success!")
    return database_list


def get_public_folder_database_by_server(m_username, m_password, host_name):
    """
    功能描述：获取公共数据库列表 只有Exchange2010可以创建该数据库
    查询是否有可以备份的数据库：
    Recovery 不备份
    """
    log.info(f"get public folder database by server start!")
    script = Commands.GET_PUBLIC_FOLDER_DATABASE_BY_SERVER.format(server=host_name)
    result = execute(m_username, m_password, script)
    database_list = []
    return_code = result.get("ReturnCode", "")
    if return_code != "0":
        log.error("Get Mailbox Database ReturnCode: %s, ErrorMessage: %s", return_code, result.get("Output", ""))
        return database_list
    if result.get("Output", []):
        result_obj = json.loads(result.get("Output"))
        # 返回结果只有一条时，创建一个list
        if not isinstance(result_obj, list):
            result_obj = [result_obj]
        for database in result_obj:
            database_info = dict()
            # 不备份Recovery 数据库
            recovery = database["Recovery"]
            if recovery:
                continue
            database_info["server"] = database["Server"]
            database_info["name"] = database["Name"]
            database_list.append(database_info)
    log.info(f"get public folder database by server success!")
    return database_list


def get_mailbox_database_copy_status(m_username, m_password, db_name):
    """
    功能描述：获取数据库信息副本信息
    查询指定数据库下：
    数据副本状态为已挂载或状态为healthy
    """
    script = Commands.GET_MAILBOX_DATABASE_COPY_STATUS.format(db_name=db_name)
    result = execute(m_username, m_password, script)
    copy_list = []
    return_code = result.get("ReturnCode", "")
    if return_code != "0":
        log.error("Get Mailbox ReturnCode: %s, ErrorMessage: %s", return_code, result.get("Output", ""))
        return copy_list
    if result.get("Output", []):
        result_obj = json.loads(result.get("Output"))
        # 返回结果只有一条时，创建一个list
        if not isinstance(result_obj, list):
            result_obj = [result_obj]
        for database_copy in result_obj:
            copy_info = dict()
            copy_info["databaseName"] = db_name
            copy_info["name"] = database_copy["Name"]
            status = database_copy["Status"]["Value"]
            # 只获取被动副本信息
            active_copy = database_copy["ActiveCopy"]
            if active_copy:
                continue
            # 仅获取状态为 Healthy和Mounted的副本信息
            if status not in ("Healthy", "Mounted"):
                continue
            copy_info["mailboxServer"] = database_copy["MailboxServer"]
            copy_info["activeDatabaseCopy"] = database_copy["ActiveDatabaseCopy"]
            copy_info["activeCopy"] = database_copy["ActiveCopy"]
            copy_info["activationPreference"] = database_copy["ActivationPreference"]
            copy_info["contentIndexState"] = database_copy["ContentIndexState"]
            copy_list.append(copy_info)
    log.info(f"Get mailbox database size: {len(copy_list)}")
    return copy_list


def get_local_mailbox_database_copy_status(m_username, m_password, db_name, server_name):
    """
    功能描述：获取数据库信息副本信息
    查询指定数据库下：
    数据副本状态为已挂载或状态为healthy
    """
    if not server_name:
        raise Exception(f"Result file: {server_name} is none")
    script = Commands.GET_MAILBOX_DATABASE_COPY_STATUS.format(db_name=db_name)
    result = execute(m_username, m_password, script)
    copy_list = []
    return_code = result.get("ReturnCode", "")
    if return_code != "0":
        log.error("Get Mailbox ReturnCode: %s, ErrorMessage: %s", return_code, result.get("Output", ""))
        return copy_list
    if result.get("Output", []):
        result_obj = json.loads(result.get("Output"))
        # 返回结果只有一条时，创建一个list
        if not isinstance(result_obj, list):
            result_obj = [result_obj]
        for database_copy in result_obj:
            copy_info = dict()
            if database_copy["MailboxServer"] != server_name.upper():
                continue
            copy_info["databaseName"] = db_name
            copy_info["name"] = db_name
            status = database_copy["Status"]["Value"]
            # 仅获取状态为 Healthy和Mounted的副本信息
            if status not in ("Healthy", "Mounted"):
                continue
            copy_info["mailboxServer"] = database_copy["MailboxServer"]
            copy_info["activeDatabaseCopy"] = database_copy["ActiveDatabaseCopy"]
            copy_info["activeCopy"] = database_copy["ActiveCopy"]
            copy_info["activationPreference"] = database_copy["ActivationPreference"]
            copy_info["contentIndexState"] = database_copy["ContentIndexState"]
            copy_list.append(copy_info)
    log.info(f"Get mailbox database size: {len(copy_list)}")
    return copy_list


def get_mounted_databases_by_server(m_username, m_password, server):
    """
    功能描述：获取数据库信息副本信息
    查询指定数据库下：
    数据副本状态为已挂载或状态为healthy
    """
    script = Commands.GET_MOUNTED_DATABASES_BY_SERVER.format(server=server)
    result = execute(m_username, m_password, script)
    copy_list = []
    return_code = result.get("ReturnCode", "")
    if return_code != "0":
        log.error("Get Mailbox ReturnCode: %s, ErrorMessage: %s", return_code, result.get("Output", ""))
        return copy_list
    if result.get("Output", []):
        result_obj = json.loads(result.get("Output"))
        # 返回结果只有一条时，创建一个list
        if not isinstance(result_obj, list):
            result_obj = [result_obj]
        for database_copy in result_obj:
            if database_copy["Status"]["Value"] == "Mounted":
                copy_list.append(database_copy["DatabaseName"])
    log.info(f"Get mailbox database size: {len(copy_list)}")
    return copy_list


def check_exchange_online_status(self):
    """
    检查exchange server运行状态
    :return: true,false
    """
    log.info('Start check exchange online status')
    # 校验exchange server在线状态结果
    if ExchangeServiceStatus.RUNNING == self._exchange_cmd_client.get_exchange_online_status():
        log.info('Exchange is online.')
        return True
    log.error('Exchange is offline.')
    return False


def get_mailbox_database_active_copy(m_username, m_password, db_name):
    """
    功能描述：获取主动数据库信息副本信息
    查询指定数据库下：
    数据状态副本状态为已激活的
    """
    script = Commands.GET_MAILBOX_DATABASE_COPY_STATUS.format(db_name=db_name)
    result = execute(m_username, m_password, script)
    copy_info = dict()
    return_code = result.get("ReturnCode", "")
    if return_code != "0":
        log.error("Get Mailbox ReturnCode: %s, ErrorMessage: %s", return_code, result.get("Output", ""))
        return copy_info
    if result.get("Output", []):
        result_obj = json.loads(result.get("Output"))
        # 返回结果只有一条时，创建一个list
        if not isinstance(result_obj, list):
            result_obj = [result_obj]
        for database_copy in result_obj:
            copy_info["databaseName"] = db_name
            copy_info["name"] = database_copy["Name"]
            # 只获取主动副本信息
            active_copy = database_copy["ActiveCopy"]
            if not active_copy:
                continue
            copy_info["mailboxServer"] = database_copy["MailboxServer"]
            copy_info["activeDatabaseCopy"] = database_copy["ActiveDatabaseCopy"]
            copy_info["activeCopy"] = database_copy["ActiveCopy"]
            copy_info["activationPreference"] = database_copy["ActivationPreference"]
            copy_info["contentIndexState"] = database_copy["ContentIndexState"]
    return copy_info


def check_exchange_mailbox_exist(m_username, m_password, mailbox_name):
    log.info(f"check_exchange_mailbox_exist start, mailbox_name:{mailbox_name}.")
    mailbox_name = keep_powershell_escape_character(mailbox_name)
    result = execute_with_output_bool(m_username, m_password,
                                      Commands.IS_MAILBOX_EXIST.format(mailbox_name=mailbox_name))
    if result.get("Output") == "True":
        return True
    else:
        return False


def get_target_path(data_hub, guid, meta_hub):
    input_data_hub = os.path.join(data_hub, f"MailboxDatabase_{guid}").replace("/", "\\")
    input_meta_hub = os.path.join(meta_hub, f"MailboxDatabase_{guid}").replace("/", "\\")
    return input_data_hub, input_meta_hub


# 获取exchange server版本
def get_exchange_version():
    # 执行命令获取Microsoft Exchange Server版本 Get-Package -Name "Microsoft Exchange Server"|Select Version
    result = execute_without_auth(Commands.GET_EXCHANGE_VERSION)
    # 命令执行失败抛出异常,记录日志抛出异常
    if result.get("ReturnCode", "") != "0":
        log.error("Get exchange version fail code: %s, ErrorMessage: %s", result.get("ReturnCode", ""),
                  result.get("Output", ""))
        raise ExchangeInternalException(result.get("Output", ""))
    # 获取exchange server版本
    return json.loads(result.get("Output"))["Version"]


def copy_backup_data(input_data_hub, log_path, snap_info, is_log=False):
    # 复制log文件，获取虚拟盘edb路径
    log.info(f"Get snapshot info {snap_info} get log path {log_path[0]}")
    log_snap_id = get_snap_id(snap_info, log_path[0])
    virtual_log_path = ParamConstant.VIRTUAL_PATH + f"{log_snap_id}" + log_path.split(':')[1]
    log.info(f"Get snapshot info {virtual_log_path}")
    log_to_time = {}
    total_copy_size = 0
    min_create_time = ParamConstant.EMPTY_DATA
    max_create_time = ParamConstant.EMPTY_DATA

    # 遍历源目录下的所有文件
    for root, _dirs, files in os.walk(virtual_log_path):
        for file in files:
            # 判断文件是否以.edb结尾
            copy_size, create_time = copy_log_and_chk(file, input_data_hub, is_log, log_to_time, root)
            if create_time != ParamConstant.EMPTY_DATA:
                max_create_time, min_create_time = get_restore_time(create_time, max_create_time, min_create_time)
            total_copy_size = total_copy_size + copy_size

    log.info(f"Get log to time {log_to_time}")
    # 日志备份场景，生成文件名 - 时间戳的映射文件
    log_to_time_file = os.path.join(input_data_hub, "log_to_time")
    if is_log:
        exec_overwrite_file(log_to_time_file, log_to_time)
    return total_copy_size, min_create_time, max_create_time, log_to_time


def get_restore_time(create_time, max_create_time, min_create_time):
    if min_create_time == ParamConstant.EMPTY_DATA or max_create_time == ParamConstant.EMPTY_DATA:
        log.info(f"Init min create time {min_create_time}, max create time {max_create_time}")
        min_create_time = create_time
        max_create_time = create_time
    else:
        log.info(f"Compare min create time {min_create_time}, max create time {max_create_time}")
        log.info(f"Compare min create time {create_time}")
        min_create_time = create_time if create_time < min_create_time else min_create_time
        max_create_time = create_time if create_time > max_create_time else max_create_time
        log.info(f"Compared min create time {min_create_time}, max create time {max_create_time}")
    return max_create_time, min_create_time


def copy_log_and_chk(file, input_data_hub, is_log, log_to_time, root):
    if file.endswith(".log") or file.endswith('.chk'):
        # 构造源文件路径和目标文件路径
        src_file = os.path.join(root, file)
        create_time = save_time_stamp(file, is_log, log_to_time, src_file)
        dst_file = os.path.join(input_data_hub, file)
        # 复制文件
        shutil.copy(ExchangeWindowsPath.UNC + src_file, dst_file)
        # 返回拷贝文件大小
        return os.path.getsize(src_file), create_time
    return 0, ParamConstant.EMPTY_DATA


def save_time_stamp(file, is_log, log_to_time, src_file):
    create_time = ParamConstant.EMPTY_DATA
    log.info(f"Get is_log {is_log}")
    if is_log and file.endswith(".log"):
        eseutil_cmd = "'{}' /ml '{}'".format(ParamConstant.ESEUTIL_TOOL_PATH, src_file)
        code, out, err = get_command_result(eseutil_cmd)
        log.info(f"Get file {file}")
        for line in out.split("\n"):
            if "creation time:" in line:
                time_str = line.split('creation time: ')[-1]
                timestamp = get_time_format(time_str)
                log.info(f"Get timestamp {timestamp}, line {line}")
                create_time = timestamp
                log_to_time[file] = timestamp
                break
    return create_time


def get_time_format(time_str):
    """
    时间格式转换，兼容2010版本
    :param time_str:
    :return:
    """
    try:
        time_obj = datetime.datetime.strptime(time_str, '%m/%d/%Y %H:%M:%S.%f %Z')
    except Exception as e:
        log.warn(f"Incorrect time format conversion, err: {e}")
        time_obj = datetime.datetime.strptime(time_str.split(".")[0], '%m/%d/%Y %H:%M:%S')
    if "UTC" in time_str:
        timestamp = int(time_obj.replace(tzinfo=datetime.timezone.utc).timestamp())
    else:
        timestamp = int(time.mktime(time_obj.timetuple()))
    return timestamp


def output_action_result(pid, code, body_err, message, err_params: list = None):
    """
    将actionResult写入到结果文件,供框架读取
    :return:
    """
    json_result = ActionResult(code=code, bodyErr=body_err, message=message, bodyErrParams=err_params)
    file_path = os.path.join(ParamConstant.RESULT_PATH, f"result{pid}")
    log.info(f"Write file.path:{file_path} action code: {code}, body_err: {body_err}, message: {message}")
    output_execution_result_ex(file_path, json_result.dict(by_alias=True))


def write_file(file_path, file_content):
    """
    功能描述：如果文件不存在, 则创建文件, 写入数据
    参数：
    @file_path: 文件路径
    @file_content: 文件内容
    返回值：无
    """
    count = 0
    while count < 3:
        count = count + 1
        try:
            if not os.path.exists(file_path):
                write_dir_file(file_content, file_path)
            break
        except Exception as e:
            log.error(e, exc_info=True)
            time.sleep(3)


def write_dir_file(file_content, file_path):
    check_path_legal(file_path)
    dirname = os.path.dirname(file_path)
    if not os.path.exists(dirname):
        exec_mkdir_cmd(dirname)
    write_content_to_file(file_path, file_content)


def output_tmp_info(file_path, payload):
    check_path_legal(file_path)
    flags = os.O_WRONLY | os.O_CREAT
    modes = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
    with os.fdopen(os.open(file_path, flags, modes), 'w') as out_file:
        out_file.write(payload)


def check_clean_dir(dir_path):
    # 清理目录下所有文件
    if not os.path.exists(dir_path):
        return
    check_path_legal(dir_path)
    for path in os.listdir(dir_path):
        new_path = os.path.join(dir_path, path)
        if os.path.isfile(new_path):
            os.remove(new_path)
        elif os.path.isdir(new_path):
            shutil.rmtree(new_path)


def check_del_dir_or_file(path):
    # 删除目录或文件
    if not os.path.exists(path):
        return
    check_path_legal(path)
    if os.path.isdir(path):
        shutil.rmtree(path, ignore_errors=True)
    if os.path.isfile(path):
        try:
            os.remove(path)
        except Exception as error:
            log.error(error, exc_info=True)


def get_env_variable(str_env_variable: str):
    env_variable = ''
    input_str = json.loads(SysData.SYS_STDIN)
    if input_str.get(str_env_variable):
        env_variable = input_str.get(str_env_variable)
    return env_variable


def get_key_value(key_name):
    if not SysData.SYS_STDIN:
        return False, ""
    file_path = os.path.join(ParamConstant.PARAM_FILE_PATH, "exchange.lock")
    file_desc = None
    try:
        flags = os.O_WRONLY | os.O_CREAT
        modes = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
        file_desc = os.fdopen(os.open(file_path, flags, modes), 'w')
        # 加文件锁
        msvcrt.locking(file_desc.fileno(), msvcrt.LK_LOCK, 1)
        file_desc.seek(0)
        json_str = json.loads(SysData.SYS_STDIN)
    except Exception as e_info:
        log.error(f"msvcrt.locking failed, err: {e_info}", exc_info=True)
        return False, ""
    finally:
        if file_desc is not None:
            msvcrt.locking(file_desc.fileno(), msvcrt.LK_UNLCK, 1)
            file_desc.close()
    try:
        username = json_str.get(key_name)
    except Exception as e_info:
        return False, ""
    if not username:
        return False, ""
    return True, username


def read_first_line(in_out):
    line_list = in_out.split('\n')
    if len(line_list) > 0:
        return line_list[0].encode('utf-8').decode('utf-8')
    else:
        return ""


def check_is_sub_job(json_param):
    return bool(json_param.get("subJob"))


def read_file(path):
    with open(path, "r", encoding='utf-8') as tmp:
        result = json.load(tmp)
    log.debug(f"Read progress end, path: {path}")
    return result


def exec_rc_tool_cmd(unique_id, interface_name, param_dict):
    """
    执行rc_tool命令
    @@param cmd: 需要执行的命令
    @@param in_param: 需要写入输入文件的命令参数
    @@param unique_id: 输入输出文件唯一标识
    @@return result:bool 命令执行结果
    @@return output:string 命令输出
    """

    def clear_file(path):
        if os.path.isfile(path):
            os.remove(path)

    cur_time = str(int((time.time())))
    unique_id = unique_id + interface_name + cur_time
    input_file_path = os.path.join(RpcParamKey.PARAM_FILE_PATH,
                                   RpcParamKey.INPUT_FILE_PREFFIX + unique_id)
    output_file_path = os.path.join(RpcParamKey.RESULT_PATH, RpcParamKey.OUTPUT_FILE_PREFFIX + unique_id + cur_time)
    exec_overwrite_file(input_file_path, param_dict)

    cmd = f"sh {RpcParamKey.RPC_TOOL} {interface_name} {input_file_path} {output_file_path}"
    try:
        ret, std_out, std_err = execute_cmd(cmd)
    except Exception as err:
        raise err
    finally:
        # 执行命令后删除输入文件
        clear_file(input_file_path)

    if ret != CMDResult.SUCCESS:
        return {}

    # 读取文件成功后删除文件
    try:
        with open(output_file_path, "r", encoding='utf-8') as tmp:
            result = json.load(tmp)
    except Exception as err:
        raise err
    finally:
        clear_file(output_file_path)

    return result


def report_job_details(job_id: str, sub_job_details: dict):
    log.info(f"Report jobid: {job_id}, sub_job_details: {sub_job_details} ")
    try:
        result_info = exec_rc_tool_cmd(job_id, "ReportJobDetails", sub_job_details)
    except Exception as err:
        log.error(f"Invoke rpc_tool interface exception, err: {err}.")
        return False
    if not result_info:
        return False
    ret_code = result_info.get("code", -1)
    if ret_code != int(CMDResult.SUCCESS):
        log.error(f"Invoke rpc_tool interface failed, result code: {ret_code}.")
        return False
    return True


def read_temp_file(file_path):
    """
    解析tmp目录下的输入文件, 转为json对象
    """
    if not os.path.isfile(file_path):
        raise Exception(f"File:{file_path} not exist")
    try:
        with open(file_path, "r", encoding='UTF-8') as f:
            json_dict = json.loads(f.read())
    except Exception as e:
        raise Exception("parse param file failed") from e
    return json_dict


def get_free_space(path):
    """
    获取目录所在盘的剩余空间, 返回值为B
    :param path: 路径
    :return: 空间大小，单位为B
    """
    usage = psutil.disk_usage(path[0:3])
    return usage.free


def get_exchange_install_path():
    return os.getenv("ExchangeInstallPath")


def create_soft_link(path_info: CreateSoftLink):
    """
    创建软链接，创建前校验源路径用户,创建前请确保目标路径不存在
    :param path_info: 路径信息，需要传入 CreateSoftLink 实例
    :return: Bool
    """
    if not os.path.exists(path_info.src_path):
        log.error("The source path does not exist.")
        return False
    if os.path.exists(path_info.dst_path):
        log.error("The soft link destination path already exists.")
        return False
    try:
        os.symlink(path_info.src_path, path_info.dst_path)
    except Exception as exception_info:
        return False
    return True


def mailbox_anonymization(raw_str):
    """
    邮箱日志脱敏，去掉@以及@后面的域名
    :param raw_str: 原始字符串
    :return: 修改后的
    """
    if raw_str.rfind("@") == -1:
        return raw_str
    else:
        return raw_str[0:raw_str.rfind("@")]


def report_job_details_by_rpc(req_id: str, pid: str, job_id: str, job_detail: dict):
    """
    功能描述：上报进度
    参数：无
    @job_id: 任务id
    @job_detail: 任务详情
    返回值：无
    """
    rpc_tool_path = f"{adaptation_win_path()}/DataBackup/ProtectClient/Plugins/GeneralDBPlugin/bin/rpctool.bat"
    random_id = f'{req_id}_{pid}' + str(uuid.uuid4())
    input_file = RpcParamKey.WINDOWS_PARAM_FILE_PATH + "/" + random_id + "_input"
    output_file = RpcParamKey.WINDOWS_RESULT_PATH + "/" + random_id + "_output"
    # 将任务状态写入文件
    output_execution_result_ex(input_file, job_detail)
    interface_name = "ReportJobDetails"
    cmd = f"{rpc_tool_path} {interface_name} {input_file} {output_file}"
    code, out, err = execute_cmd(cmd)
    with open(output_file, 'r', encoding='utf-8') as file_read:
        result = json.load(file_read)
    remove_file(input_file)
    remove_file(output_file)
    ret = result.get("code", -1)
    if ret != ExchangeCode.SUCCESS.value:
        log.error(f"{interface_name} failed, job id: {job_id}, ret: {ret}")
        return False
    else:
        log.info(f"{interface_name} success, job id: {job_id}")
        return True


def remove_file(file_path):
    if os.path.exists(file_path):
        os.remove(file_path)
    else:
        log.info(f"{file_path} is not exist, needn't remove")


# 定义函数，计算文件大小
def get_file_size(file_path):
    size = os.path.getsize(file_path)
    return size


# 定义函数，遍历目录下的所有文件和子目录
def traverse_folder(folder_path):
    total_size = 0
    for root, _, files in os.walk(folder_path):
        for file in files:
            file_path = os.path.join(root, file)
            size = get_file_size(file_path)
            total_size += size
    return total_size


def convert_iso_time_to_timestamp(iso_time: str) -> int:
    dt = datetime.datetime.strptime(iso_time, '%Y-%m-%dT%H:%M:%S%z')
    return int(dt.timestamp())


# 检查windows路径合法性
def check_win_path_legality(path):
    """
    `^` 表示字符串的开头
    `$` 表示字符串的结尾
    `\\` 表示转义字符 `\`
    `[a-zA-Z]:` 匹配盘符
    `[^\\/:*?"<>|\r]+` 匹配文件夹或文件名
    :param path:
    :return:
    """
    pattern = r'^(([a-zA-Z]:\\)|\\\\)([^\\/:*?"<>|\r]+\\)*([^\\/:*?"<>|\r]+)?$'
    return bool(re.match(pattern, path))


def keep_powershell_escape_character(param: str):
    """
    转义字符保留
    :param param:
    :return:
    """
    result = param.replace("`", "``")
    return result


def escape_sqlite_character(param: str):
    """
    转义字符
    :param param:
    :return:
    """
    result = param.replace("'", "''")
    return result


def get_windows_server_version() -> str:
    result = execute_without_auth(Commands.GET_WINDOWS_SERVER_VERSION)
    return_code = result.get("ReturnCode", "")
    output = result.get("Output", "")
    if return_code != '0':
        log.error(f"Get windows server version failed, code: {return_code}, output: {output}")
        return ""
    return json.loads(output).get("Version", "")


def convert_byte_to_mb(capacity):
    return int(capacity / 1024 / 1024)


def get_percentage_data(data, percent):
    return int(data * percent / 100)


def get_exchange_conf_path():
    """
     功能描述：获取exchange配置文件地址
        Returns:
           str: 文件地址字符串
    """
    exchange_conf_path = f"{get_install_head_path()}" \
                         f"/DataBackup/ProtectClient/Plugins/GeneralDBPlugin/bin/applications/exchange/exchange.conf"
    return exchange_conf_path


def get_exchange_config():
    """
    功能描述：获取exchange下配置文件内容
       Returns:
           dict: 配置文件内容
    """
    exchange_conf_path = get_exchange_conf_path()
    try:
        with open(exchange_conf_path, "r") as temp_f:
            exchange_config = json.loads(temp_f.read())
    except Exception as err:
        log.error("Fail to read exchange conf for %s", err)
        return {}
    return exchange_config
